在此项目中,主要介绍了如何实现一个 JIT 编译器。JIT 的主要含义是在运行时生成机器码,然后执行。
实现一个无参数函数的调用
这里,实现的代码主要是将一串字符串打印到控制台。也就是说,代码功能等同于:
std::string hello_name = "Hello, Bob";
std::cout<<hello_name;
由于 std::cout
实际上是一个对象,因此我们这里使用 write
实现打印功能,上述代码被替换为:
std::string hello_name = "Hello, Bob";
// 1 代表了 STDOUT
write(1, hello_name.c_str(), hello_name.size());
下面的工作就是使用汇编代替对 write 的调用,首先写出下面的代码:
# chunk.s
.intel_syntax noprefix
# 调用 write 系统调用(man 2 write)
# ssize_t write(int fd, const void *buf, size_t count);
mov rax, 1 # 系统调用号
# 将函数参数放到 rdi, rsi, rdx, r10, r8, r9 寄存器中
mov rdi, 1
lea rsi, [rip + 0xa]
mov rdx, 0x11
syscall
ret
.string "Hello, Your Name\n"
然后运行下面的代码查看生成的汇编:
as chunk.s -o chunk.o
objdump -M intel -D chunk.o
得到的结果为:
0: 48 c7 c0 01 00 00 00 mov rax,0x1 7: 48 c7 c7 01 00 00 00 mov rdi,0x1 e: 48 8d 35 0a 00 00 00 lea rsi,[rip+0xa] (1) 15: 48 c7 c2 11 00 00 00 mov rdx,0x11 (2) 1c: 0f 05 syscall 1e: c3 ret
1 | 指向字符串的位置 |
2 | 0x11 代表了字符串的长度 |
然后将其储存到 std::vector
中:
std::vector<uint8_t> machine_code = {
0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1
0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1
0x48, 0x8d, 0x35, 0x0a, 0x00, 0x00, 0x00, // lea rsi, [rip + 0xa]
0x48, 0xc7, 0xc2, 0x11, 0x00, 0x00, 0x00, // mov rdx, 0x11
0x0f, 0x05, // syscall
0xc3 // ret
};
这里请看第四行第四列中的 0x11,这代表了字符串的长度。在上面的汇编代码中,我们将字符串的位置放在了代码的尾部,因此这里也直接将字符串追加到 machine_code 的尾部即可:
for(auto ch: hello_name) {
machine_code.emplace_back(ch);
}
uint32_t len = hello_name.length();
memcpy(&machine_code[24], &len, 4); // 传入字符串的长度
现在汇编代码已经生成了,下面只需要将汇编代码拷贝到内存中执行就行了。由于内存权限的原因,汇编代码所在的区域必须是:
大小必须为
sysconf(_SC_PAGE_SIZE)
的整数倍内存区域必须是可读可执行的
因此我们直接用 mmap 分配一块内存区域,然后将汇编代码拷贝过去:
size_t estimate_memory_size(size_t machine_code_size) {
size_t b = sysconf(_SC_PAGE_SIZE);
return (machine_code_size + b - 1) / b * b;
}
int main(){
// ...
size_t required_mem_size = estimate_memory_size(machine_code.size());
uint8_t* mem = (uint8_t*)mmap(nullptr, required_mem_size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(mem == MAP_FAILED) {
std::cerr << "分配内存失败\n";
return 1;
}
// 拷贝汇编代码
memcpy(mem, &machine_code[0], machine_code.size());
}
注意函数 estimate_memory_size,其中用到了一个向上取整算法 (A+B-1)/B
,先除以 B 再乘以 B 是必要的,(A+B-1)/B 最后会向下取整。
最后直接执行就可以了:
auto func = reinterpret_cast<void (*)()>(mem);
func();
munmap(mem, required_mem_size);
完整的代码为:
#include <iostream>
#include <string>
#include <vector>
extern "C" {
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
}
size_t estimate_memory_size(size_t machine_code_size) {
size_t b = sysconf(_SC_PAGE_SIZE);
return (machine_code_size + b - 1) / b * b;
}
int main(int argc, char* argv[]) {
std::string hello_name = "bob";
// clang-format off
std::vector<uint8_t> machine_code = {
0x48, 0xc7, 0xc0, 0x01, 0x00, 0x00, 0x00, // mov rax, 1
0x48, 0xc7, 0xc7, 0x01, 0x00, 0x00, 0x00, // mov rdi, 1
0x48, 0x8d, 0x35, 0x0a, 0x00, 0x00, 0x00, // lea rsi, [rip + 0xa]
0x48, 0xc7, 0xc2, 0x11, 0x00, 0x00, 0x00, // mov rdx, 0x11
0x0f, 0x05, // syscall
0xc3 // ret
};
// clang-format on
uint32_t len = hello_name.length();
memcpy(&machine_code[24], &len, 4);
for(auto ch: hello_name) {
machine_code.emplace_back(ch);
}
size_t required_mem_size = estimate_memory_size(machine_code.size());
uint8_t* mem = (uint8_t*)mmap(nullptr, required_mem_size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(mem == MAP_FAILED) {
std::cerr << "分配内存失败\n";
return 1;
}
memcpy(mem, &machine_code[0], machine_code.size());
auto func = reinterpret_cast<void (*)()>(mem);
func();
munmap(mem, required_mem_size);
return 0;
}
从汇编代码中调用 CPP 函数
在上面的代码中,我们运行时生成汇编代码,然后从 cpp 中调用,这里我们使用汇编代码调用 cpp 代码。大部分内容和上面的代码相同,但是汇编代码使用的是:
.intel_syntax noprefix
# 调用函数
push rbp
mov rbp, rsp
# 传入参数地址
movabs rax, 0x0
call rax
# 返回
pop rbp
ret
汇编之后的代码为:
std::vector<uint8_t> machine_code = {
0x55, // push rbp
0x48, 0x89, 0xe5, // mov rbp,rsp
0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs rax,0x0
0x00, 0x00, 0x00,
0xff, 0xd0, // call rax
0x5d, // pop rbp
0xc3, // ret
};
这里需要注意的是,movabs rax
的汇编代码为 0x48, 0xb8
,后面的 8 个字节为零的内容为函数指针地址(因为 x64 计算机上指针宽度为 8 字节)。然后我们只需要将函数地址拷贝到相应的地方就行了:
uint64_t address = reinterpret_cast<uint64_t>(&test);
memcpy(&machine_code[6], &address, sizeof(address));
最后完整的代码为:
#include <iostream>
#include <string>
#include <vector>
extern "C" {
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
}
size_t estimate_memory_size(size_t machine_code_size) {
size_t b = sysconf(_SC_PAGE_SIZE);
return (machine_code_size + b - 1) / b * b;
}
std::vector<int> datas { 1, 2, 3 };
void test() {
for(int data: datas) {
std::cout << data << ' ';
}
std::cout << std::endl;
}
int main(int argc, char* argv[]) {
// clang-format off
std::vector<uint8_t> machine_code = {
0x55, // push rbp
0x48, 0x89, 0xe5, // mov rbp,rsp
0x48, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, // movabs rax,0x0
0x00, 0x00, 0x00,
0xff, 0xd0, // call rax
0x5d, // pop rbp
0xc3, // ret
};
// clang-format on
uint64_t address = reinterpret_cast<uint64_t>(&test);
memcpy(&machine_code[6], &address, sizeof(address));
size_t required_mem_size = estimate_memory_size(machine_code.size());
uint8_t* mem = (uint8_t*)mmap(nullptr, required_mem_size, PROT_READ | PROT_WRITE | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if(mem == MAP_FAILED) {
std::cerr << "分配内存失败\n";
return 1;
}
memcpy(mem, &machine_code[0], machine_code.size());
auto func = reinterpret_cast<void (*)()>(mem);
func();
munmap(mem, required_mem_size);
return 0;
}